Custom Derive Macro
A custom derive macro is a procedural macro that runs when you write:
#[derive(MyTrait)]
struct MyType { ... }
It:
- Receives the full definition of the struct/enum.
- Analyzes its fields, variants, generics, attributes, etc.
- Generates an implementation of a trait (or other code).
Why Use Custom Derives?
They are perfect for:
- Automatically implementing traits based on structure.
- Reducing boilerplate.
- Enforcing compile-time rules.
- Creating declarative APIs.
Examples from the ecosystem:
#[derive(Serialize, Deserialize)]→serde#[derive(Parser)]→clap#[derive(Error)]→thiserror
How Custom Derive Macros Work
- The compiler sees
#[derive(MyTrait)]. - It sends the annotated item as a
TokenStreamto your macro. - Your macro:
- Parses the input into a syntax tree (AST).
- Extracts information (name, fields, generics).
- Generates Rust code.
- The generated code is compiled as if the user wrote it.
Step-by-Step: Build a Custom Derive Macro
We’ll build #[derive(HelloMacro)] that adds a method to a struct or enum.
Step 1: Create the Proc-Macro Crate
cargo new hello_macro_derive --lib
In Cargo.toml:
[lib]
proc-macro = true
[dependencies]
syn = "2"
quote = "1"
proc-macro2 = "1"
Step 2: Define the Macro Code
In lib.rs:
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
// Parse input tokens into a syntax tree
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
// Generate code
let expanded = quote! {
impl HelloMacro for #name {
fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
};
TokenStream::from(expanded)
}
Step 3: Define the Trait (in a normal crate)
pub trait HelloMacro {
fn hello();
}
Step 4: Use the Derive Macro
use hello_macro_derive::HelloMacro;
#[derive(HelloMacro)]
struct Pancakes;
fn main() {
Pancakes::hello();
}
What Gets Generated
Conceptually, the compiler expands this into:
impl HelloMacro for Pancakes {
fn hello() {
println!("Hello from Pancakes!");
}
}
Example 2: Derive Macro That Reads Struct Fields
Let’s create a more realistic macro:
#[derive(Describe)] that prints the field names and types.
Macro Implementation
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Data, DeriveInput, Fields};
#[proc_macro_derive(Describe)]
pub fn describe_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let fields = match input.data {
Data::Struct(data) => match data.fields {
Fields::Named(fields) => fields.named,
_ => panic!("Describe only supports structs with named fields"),
},
_ => panic!("Describe only supports structs"),
};
let field_names = fields.iter().map(|f| {
let name = &f.ident;
quote! {
println!("Field: {}", stringify!(#name));
}
});
let expanded = quote! {
impl Describe for #name {
fn describe() {
println!("Struct {}", stringify!(#name));
#(#field_names)*
}
}
};
TokenStream::from(expanded)
}
Trait Definition
pub trait Describe {
fn describe();
}
Usage
#[derive(Describe)]
struct User {
id: u32,
name: String,
active: bool,
}
fn main() {
User::describe();
}
Expansion (Conceptual)
impl Describe for User {
fn describe() {
println!("Struct User");
println!("Field: id");
println!("Field: name");
println!("Field: active");
}
}
Handling Generics in Derive Macros
If your struct has generics:
#[derive(HelloMacro)]
struct Wrapper<T> {
value: T,
}
You must carry generics into the impl:
let generics = input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
quote! {
impl #impl_generics HelloMacro for #name #ty_generics #where_clause {
fn hello() {
println!("Hello from {}!", stringify!(#name));
}
}
}
Supporting Attributes in Custom Derives
You can let users customize behavior:
#[derive(Describe)]
#[describe(skip)]
struct User {
id: u32,
password: String,
}
Then parse attributes with syn to change code generation (e.g., skip fields).
Error Handling in Derive Macros
Instead of panic!, use compile errors:
return syn::Error::new_spanned(
input,
"Describe only supports structs with named fields",
)
.to_compile_error()
.into();
This gives users friendly compiler messages.
When to Use Custom Derives vs Other Macros
| Use Case | Best Tool |
|---|---|
| Implement trait based on struct shape | Custom derive |
| Wrap or modify functions | Attribute macro |
| Custom DSL or syntax | Function-like macro |
| Simple repetition | macro_rules! |